﻿using System;
using System.Collections.Concurrent;
using System.IO;
using System.Reflection;

namespace Shen.NET
{
    public class Context
    {
        public static Context NewGlobal()
        {
            var c = new Context();
            c.Parent = c;
            c.Global = c;
            Builtins.Install(c);
            return c;
        }

        public Context Parent { get; private set; }
        public Context Global { get; private set; }

        private Context()
        {
        }

        private Context(Context parent)
        {
            Parent = parent;
            Global = parent.Global;
        }

        private readonly ConcurrentDictionary<String, Object> _defs = new ConcurrentDictionary<String, Object>();

        public Symbol Intern(String id)
        {
            return Global._defs.GetOrAdd(id, _ => new Symbol(id)).As<Symbol>();
        }

        private Object InternalLookup(String id)
        {
            if (_defs.ContainsKey(id))
                return _defs[id];

            return this == Parent ? null : Parent.InternalLookup(id);
        }

        public Object Resolve(String id)
        {
            return InternalLookup(id) ?? Intern(id);
        }
        
        public Context With(String id, Object value)
        {
            var newContext = new Context(this);
            newContext._defs[id] = value;
            return newContext;
        }

        public Context With(String[] ids, Object[] values)
        {
            var newContext = new Context(this);

            for (var i = 0; i < ids.Length; ++i)
                newContext._defs[ids[i]] = values[i];

            return newContext;
        }
    }

    public static class Builtins
    {
        public static Context Install(Context c, DateTime? startTime = null, Stream sin = null, Stream sout = null)
        {
            sin = sin ?? Console.OpenStandardInput();
            sout = sout ?? Console.OpenStandardOutput();

            c.Intern("*language*").Value = "C# " + Assembly.GetAssembly(typeof(object)).GetName().Version;
            c.Intern("*implementation*").Value = "CLR " + Environment.Version;
            c.Intern("*release*").Value = "4.5";
            c.Intern("*port*").Value = "0.0";
            c.Intern("*porters*").Value = "Bort";
            c.Intern("*version*").Value = "13.2.1";
            c.Intern("*stinput*").Value = sin;
            c.Intern("*stoutput*").Value = sout;
            c.Intern("*home-directory*").Value = Environment.CurrentDirectory;
            c.Intern("intern").Value = Builtin.Create((String x) => c.Global.Intern(x));
            c.Intern("pos").Value = Builtin.Create((String str, Decimal n) => str[n.AsInt()] + "");
            c.Intern("tlstr").Value = Builtin.Create((String str) => str.Substring(1));
            c.Intern("cn").Value = Builtin.Create((String x, String y) => x + y);
            c.Intern("str").Value = Builtin.Create((Object x) => x.ToString());
            c.Intern("string?").Value = Builtin.Create((Object x) => x is String);
            c.Intern("n->string").Value = Builtin.Create((Decimal n) => n.AsString());
            c.Intern("string->n").Value = Builtin.Create((String str) => str[0].AsNumber());
            c.Intern("cons").Value = Builtin.Create((Object x, Object y) => new Cons(x, y));
            c.Intern("hd").Value = Builtin.Create((Cons x) => x.Car);
            c.Intern("tl").Value = Builtin.Create((Cons x) => x.Cdr);
            c.Intern("cons?").Value = Builtin.Create((Object x) => x is Cons && ! ReferenceEquals(Cons.EmptyList, x));
            c.Intern("=").Value = Builtin.Create((Object x, Object y) => Equals(x, y));
            c.Intern("type").Value = Builtin.Create((Object x) => x.GetType());
            c.Intern("absvector").Value = Builtin.Create((Decimal size) => new Vector(size, c));
            c.Intern("<-address").Value = Builtin.Create((Vector vec, Decimal n) => vec[n]);
            c.Intern("address->").Value = Builtin.Create((Vector vec, Decimal n, Object val) => {
                vec[n] = val;
                return vec;
            });
            c.Intern("absvector?").Value = Builtin.Create((Object x) => x is Vector);
            c.Intern("set").Value = Builtin.Create((Symbol sym, Object val) => sym.Value = val);
            c.Intern("value").Value = Builtin.Create((Symbol sym) => sym.Value);
            c.Intern("simple-error").Value = Builtin.Create((String message) => { throw new ApplicationException(message); });
            c.Intern("error-to-string").Value = Builtin.Create((Error e) => e.Message);
            var start = startTime ?? DateTime.UtcNow;
            var epoch = new DateTime(1970, 1, 1, 0, 0, 0, 0).ToUniversalTime();
            c.Intern("get-time").Value = Builtin.Create((Symbol x) =>
                (decimal) Math.Floor((DateTime.UtcNow - (x.Name == "unix" ? epoch : start)).TotalSeconds));
            c.Intern("write-byte").Value = Builtin.Create((Decimal b, Stream stream) => {
                stream.WriteByte(b.AsByte());
                stream.Flush();
                return b;
            });
            c.Intern("read-byte").Value = Builtin.Create((Stream stream) => (Decimal) stream.ReadByte());
            c.Intern("open").Value = Builtin.Create((String path, Symbol direction) => {
                if (!Path.IsPathRooted(path))
                    path = Path.Combine(c.Intern("*home-directory*").Value.ToString(), path);

                if (direction.Name == "in") return File.OpenRead(path);
                if (direction.Name == "out") return File.OpenWrite(path);

                throw new Exception("Invalid stream direction");
            });
            c.Intern("close").Value = Builtin.Create((Stream stream) => {
                stream.Close();
                return Cons.EmptyList;
            });
            c.Intern("stinput").Value = sin;
            c.Intern("stoutput").Value = sout;
            c.Intern("+").Value = Builtin.Create((Decimal x, Decimal y) => x + y);
            c.Intern("-").Value = Builtin.Create((Decimal x, Decimal y) => x - y);
            c.Intern("*").Value = Builtin.Create((Decimal x, Decimal y) => x * y);
            c.Intern("/").Value = Builtin.Create((Decimal x, Decimal y) => x / y);
            c.Intern(">").Value = Builtin.Create((Decimal x, Decimal y) => x > y);
            c.Intern("<").Value = Builtin.Create((Decimal x, Decimal y) => x < y);
            c.Intern(">=").Value = Builtin.Create((Decimal x, Decimal y) => x >= y);
            c.Intern("<=").Value = Builtin.Create((Decimal x, Decimal y) => x <= y);
            c.Intern("number?").Value = Builtin.Create((Object x) => x is Decimal);
            c.Intern("eval-kl").Value = Builtin.Create((Cons kl) => kl.ToToken().Parse(true).Eval(c));
            return c;
        }
    }
}
